<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MinIO Plus Demo</title>
    <!-- Import style -->
    <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
    <!-- Import Vue 3 -->
    <script src="//unpkg.com/vue@3"></script>
    <!-- Import component library -->
    <script src="//unpkg.com/element-plus"></script>
    <script src="js/spark-md5.js"></script>
</head>


<body>

<div id="app">

    <div class="el-row">
        <div class="el-col el-col-6 is-guttered">1</div>
        <div class="el-col el-col-6 is-guttered">2</div>
        <div class="el-col el-col-6 is-guttered">3</div>
        <div class="el-col el-col-6 is-guttered">4</div>
    </div>


    <div>
        <label for="loginUser">登录用户：</label><input type="text" v-model="loginUser" id="loginUser">
    </div>
    <br>
    <input type="file" @click="clearState" id="upload">
    <button class="el-button el-button--primary" @click="checkFile">检查</button>
    <button @click="uploadFile(false)" :disabled="partList.length === 0">正常上传</button>
    <button @click="uploadFile(true)" :disabled="partList.length < 2">模拟丢片上传</button>
    <button @click="recover" :disabled="missChunkNumber == null">丢片恢复</button>
<!--    <button @click="remove" :disabled="uploadId == null">删除文件</button>-->
    <div>
        <input type="text" v-model="uploadId" id="uploadId">
        <button @click="merge" :disabled="uploadId == null">合并分片</button>
    </div>
    <div>
        <button @click="preview" :disabled="uploadId == null">预览图片</button>
        <button @click="download" :disabled="uploadId == null">下载文件</button>
        <img :src="previewUrl" alt="预览图">
    </div>
    <div>
        <span>执行过程：</span><textarea id="logstr" style="height: 300px;width: 600px;"></textarea>
    </div>
    <div>
        <span>总计片数：{{partList.length}}</span>
        <div v-for="(item,index) in partList" :style="{color:missChunkNumber === index ? 'red' : 'black'}">
            <span>第{{index + 1}}片:</span>
            <br/>
            <span>{{item.url}}</span>
        </div>
    </div>

</div>
</body>
</html>

<script>
    const {createApp, reactive, toRefs, onMounted} = Vue
    createApp({
        setup() {
            const state = reactive({
                uploadId: null,
                partList: [],
                partMd5Map:{},
                missChunkNumber: null,
                partCount: null,
                partSize: null,
                fileSize: null,
                previewUrl: null,
                loginUser:'mockUser01'
            })
            /**
             * 文件检查
             * @returns {Promise<void>}
             */
            const checkFile = async () => {
                //获取用户选择的文件
                const file = document.getElementById("upload").files[0];
                //获取文件md5
                let startTime = new Date();
                console.log('开始计算文件MD5值')

                fetch("/storage/upload/sharding", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        fileSize: file.size
                    })
                }).then(res => res.json()).then(({data}) => {
                    console.log(data);

                    let fileReader = new FileReader();
                    let md5 = new SparkMD5();
                    let md5Total = new SparkMD5();
                    let currentIndex = 0;

                    const loadFile = () => {
                        if (currentIndex >= data.partList.length) {
                            let fileMd5 = md5Total.end();
                            console.log("文件md5：" + fileMd5 + "，耗时" + (new Date() - startTime) + "毫秒");

                            console.log("向后端请求本次分片上传初始化")

                            fetch("/storage/upload/init", {
                                method: "POST",
                                headers: {
                                    "Content-Type": "application/json",
                                    "Authorization":state.loginUser
                                },
                                body: JSON.stringify({
                                    fileMd5: fileMd5,
                                    fullFileName: file.name,
                                    fileSize: file.size
                                })
                            }).then(res => res.json()).then(({data}) => {
                                console.log(data);
                                // 获取文件上传id
                                state.uploadId = data.fileKey;
                                // 获取文件分片
                                state.partList = data.partList;
                                // 获取文件大小
                                state.fileSize = data.fileSize;
                                // 获取块大小
                                state.partSize = data.partSize;
                                // 获取文件分片数
                                state.partCount = data.partCount;
                            }).catch(err => {
                                console.log(err);
                            })
                            return;
                        }

                        const item = data.partList[currentIndex];
                        const slice = file.slice(item.startPosition, item.endPosition);
                        fileReader.readAsBinaryString(slice);
                        fileReader.onload = e => {
                            md5.appendBinary(e.target.result);
                            md5Total.appendBinary(e.target.result);
                            let partMd5 = md5.end();
                            console.log("开始计算第" + currentIndex + "个分片MD5值"+partMd5);
                            state.partMd5Map[item.startPosition + '_' + item.endPosition] = partMd5;
                            currentIndex++;
                            loadFile();
                        };
                    }

                    loadFile();
                }).catch(err => {
                    console.log(err);
                })


            }
            /**
             * 上传文件
             * @param isMissingPart 是否模拟丢片
             * @returns {Promise<void>}
             */
            const uploadFile = async (isMissingPart) => {
                if (isMissingPart) {
                    // 随机一个缺失分片
                    state.missChunkNumber = Math.floor(Math.random() * state.partList.length);
                } else {
                    state.missChunkNumber = null;
                }
                state.fileChunkMd5 = [];
                const file = document.getElementById("upload").files[0];
                for (const [i, item] of state.partList.entries()) {
                    //取文件指定范围内的byte，从而得到分片数据
                    let _chunkFile = file.slice(item.startPosition, item.endPosition)
                    if (i === state.missChunkNumber) {
                        console.log("丢弃第" + i + "个分片")
                        continue;
                    }
                    console.log("开始上传第" + i + "个分片", _chunkFile)
                    // 上传分片
                    fetch(item.url, {
                        method: "PUT",
                        body: _chunkFile,
                    }).then(res => {
                        console.log("第" + i + "个分片上传完成")
                    }).catch(err => {
                        console.log(err);
                    })
                }

                console.log(state.fileChunkMd5);
            }

            /**
             * 请求后端合并文件
             */
            const merge = async () => {
                console.log("开始请求后端合并文件")
                // 每块文件MD5
                const partMd5List = [];
                // 获取用户选择的文件
                const file = document.getElementById("upload").files[0];
                for (let i = 0; i < state.partCount; i++) {
                    console.log(i)
                    let _chunkFile;
                    if (i === state.missChunkNumber) {
                        continue;
                    }
                    if (i === state.partCount - 1) {
                        partMd5List.push(state.partMd5Map[i * state.partSize+'_'+ state.fileSize]);
                    } else {
                        partMd5List.push(state.partMd5Map[i * state.partSize+'_'+ (i + 1) * state.partSize]);
                    }

                }
                //
                fetch(`/storage/upload/complete/${state.uploadId}`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                        "Authorization":state.loginUser
                    },
                    body: JSON.stringify({
                        partMd5List: partMd5List
                    })
                }).then(res => res.json()).then(({data}) => {
                    console.log("合并文件完成", data)
                }).catch(err => {
                    console.log(err);
                });
            }

            const recover = async () => {
                await checkFile();
                await uploadFile(false);
                await merge();
            }
            const clearState = async () => {
                state.uploadId = null;
                state.partList = [];
                state.fileChunkMd5 = [];
                state.missChunkNumber = null
            }

            /**
             * 下载文件
             */
            const download = () => {

                fetch(`/storage/download/${state.uploadId}`, {
                    method: "GET",
                    headers: {
                        "Authorization":state.loginUser
                    },
                    redirect:'follow'
                }).then(data => {
                    console.log(data);
                    window.location.href = data.url;
                }).catch(err => {
                    console.log(err);
                });


            }
            // 图片预览
            const preview = () => {

                fetch(`/storage/preview/${state.uploadId}`, {
                    method: "GET",
                    headers: {
                        "Authorization":state.loginUser
                    },
                    redirect:'follow'
                }).then(data => {
                    console.log(data);
                    state.previewUrl = data.url;
                }).catch(err => {
                    console.log(err);
                });

            }
            /**
             * 获取文件MD5
             * @param file
             * @returns {Promise<unknown>}
             */
            const getFileMd5 = (file) => {
                let fileReader = new FileReader()
                fileReader.readAsBinaryString(file)
                let spark = new SparkMD5()
                return new Promise((resolve) => {
                    fileReader.onload = (e) => {
                        spark.appendBinary(e.target.result)
                        resolve(spark.end())
                    }
                })
            }

            return {
                checkFile,
                // removeTaskId,
                clearState,
                uploadFile,
                merge,
                download,
                preview,
                // remove,
                recover,
                ...toRefs(state)
            }
        }
    }).mount('#app')
</script>
